package pl.szpadel.android.gadu;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import pl.szpadel.android.gadu.packets.BasePacket;
import pl.szpadel.android.gadu.packets.ReceivedPacket;
import pl.szpadel.android.gadu.packets.SendPacket;
import pl.szpadel.android.gadu.packets.ZeroLenghtSendPacket;

/// Connection thread. Responsible for connection management, network communication,
/// receiving messages from UI thread and sending received packets and connection status to UI thread
public class CommunicationThread extends Thread {

	/////////////////////////////////////////////////////////
	// data
	
	/// Parent service
	CommunicationService mService;
	
	/// Connector
	private Connector mConnector;
	
	/// flag - if reconnection should be attempted
	private boolean mReconnect = true;
	
	// flag - stop working.
	//private boolean mQuit = false;
	
	/// comm channel
	//SocketChannel mChannel;
	
	/// Inter-thread message queue
	/// connection thread is the receiver, other threads are feeding into the queue
	private BlockingQueue<InterThreadMessage> mInterthreadQueue = new LinkedBlockingQueue<InterThreadMessage>();
	
	//////////////////////////////////////////////////////////
	// types
	
	/// Thread message type. USed for inter-thread communication
	private static class InterThreadMessage {
		/// message type
		public int type;
		
		// known types
		
		// commands from ui thread
		public static final int TYPE_RECONECT_NOW = 0; // command - reconnect now
		public static final int TYPE_DISCONNECT = 1; // command - disconnect
		public static final int TYPE_SEND_PACKET = 2; // command - send packet
		public static final int TYPE_QUIT = 3; // stops the comm thread
		
		// info from reading thread
		public static final int TYPE_DISCONNECTED = 100; // info - connection broken
		
		/// general purpose fields
		public SendPacket sendPacket;
		
		public InterThreadMessage(int t) {
			type = t;
		}
	}
	
	////////////////////////////////
	// Receiving thread type
	private class ReceivingThread extends Thread {
		
		/// channel to read from
		private ReadableByteChannel mChannel;
		
		/// c-tor
		ReceivingThread(ReadableByteChannel channel) {
			super();
			assert(channel != null);
			mChannel = channel;
		}
		
		/// the routine
		public void run() {
			try {
				while(true) {
					AGLog.d(CommunicationService.TAG, "rth: receiving packet");
					ReceivedPacket packet = ReceivedPacket.createFromChannel(mChannel);
					AGLog.i(CommunicationService.TAG, "rth: received packet: " + packet); 
					handlePacket(packet);
				}
			} catch (ClosedChannelException e) {
				AGLog.i(CommunicationService.TAG, "rth:channel closed, ending");
			} catch(Exception e) {
				String msg =  "rth:comm error, sending itm and ending: exception:" + e.toString() + ", \nmsg:";
				for (StackTraceElement st : e.getStackTrace()) {
					msg += "\n    > " + st;
				}
				
				AGLog.i(CommunicationService.TAG, msg);
				
				mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_DISCONNECTED));
			}
		}
	}
	
	/////////////////////////////////////////////////
	// intereface
	
	/// C-tor
	public CommunicationThread(CommunicationService service) {
		super("connection thread");
		mService = service;
		mConnector = new Connector(mService);
	}

	/// Routine
	public void run() {
		// try connecting
		mReconnect = false; // wait for command
		SocketChannel channel = null;
		ReceivingThread receivingThread = null;
		long lastPingTime = 0;
		boolean quit = false;
		try {
		do {
			AGLog.i(CommunicationService.TAG, "th:main loop. channel is null:" + (channel==null) + ", reconnect=" + mReconnect);
			if (mReconnect && channel == null) {
				channel = tryConnecting();
				receivingThread = null;
				if (channel != null) {
					try {
						AGLog.i(CommunicationService.TAG, "th: connected, starting receiving channel");
						receivingThread = new ReceivingThread(channel);
						receivingThread.start();
						
						// connected now
						sendConnectionState(ConnectionState.Connected());
						lastPingTime = System.currentTimeMillis();
					} catch (Exception e) {
						AGLog.e(CommunicationService.TAG, "th: failed to start rerading thread:"+e.getMessage());
						channel = null; // will try again...
					}
				} else {
					AGLog.e(CommunicationService.TAG, "failed to connect");
				}
			}
			// wait for events
			AGLog.d(CommunicationService.TAG, "waiting for itm");
			
			try {
				// exit after 10s to i.e. to retry reconnecting
				InterThreadMessage itm = mInterthreadQueue.poll(60, TimeUnit.SECONDS);
				
				// any itms?
				if (itm != null){
					AGLog.d(CommunicationService.TAG, "handling itm of type=" + itm.type);
					switch(itm.type) {
					case InterThreadMessage.TYPE_QUIT:
						quit = true;
						break;
					case InterThreadMessage.TYPE_RECONECT_NOW:
						closeChannelAndJoin(channel, receivingThread);
						sendConnectionState(ConnectionState.Connecting());
						channel = null;
						mReconnect = true;
						break;
					case InterThreadMessage.TYPE_DISCONNECT:
						closeChannelAndJoin(channel, receivingThread);
						sendConnectionState(ConnectionState.Disconnected(ConnectionState.REASON_DISCONNECTED_BY_USER));
						channel = null;
						mReconnect = false;
						break;
					case InterThreadMessage.TYPE_DISCONNECTED:
						closeChannelAndJoin(channel, receivingThread);
						channel = null;
						mReconnect = true;
						break;
					case InterThreadMessage.TYPE_SEND_PACKET:
						// send only if channel connected
						try {
							if (channel != null) {
								assert(itm.sendPacket != null);
								itm.sendPacket.toChannel(channel);
								
								AGLog.i(CommunicationService.TAG, "th: Sending packet: " + itm.sendPacket);
							}
						} catch (Exception e ) {
							AGLog.e(CommunicationService.TAG, "th: failed to send packet " + itm.sendPacket);
							// actual error will be handled in receiving thread
						}
					default:
						; // do nothing
					} // switch
				} // if itm
				
				// ping time?
				if (channel != null) {
					long timeFromLastPing = System.currentTimeMillis() - lastPingTime;
					if (timeFromLastPing >= 4*60*1000) { // 4 minutes
						try {
							AGLog.i(CommunicationService.TAG, "th: sending PING");
							ZeroLenghtSendPacket ping = new ZeroLenghtSendPacket(BasePacket.TYPE_PING);
							ping.toChannel(channel);
							lastPingTime = System.currentTimeMillis();
						} catch (IOException e ) {
							AGLog.e(CommunicationService.TAG, "th: error sending ping: " + e.getMessage() );
							// errors are handled by reading thread
						}
					}
				}
				
			} catch (Exception e) {
				AGLog.e(CommunicationService.TAG, "th:Error reading/handling itm: " + e.getMessage());
			} 
			
		} while(quit==false);
		} catch(Exception e) {
			AGLog.e(CommunicationService.TAG, "th: unhandled exception in main loop");
		}
		
		AGLog.i(CommunicationService.TAG, "th: thread exists");
		
		// wait for receiving thread
		closeChannelAndJoin(channel, receivingThread);
		
	}
	
	/// Stops and joins the thread
	/// call from ui thread
	public void stopCommThread() {
		AGLog.i(CommunicationService.TAG, "th:stopCommThread()");
		mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_QUIT));
		try {
			join();
		} catch (InterruptedException e) {
			; // nothing
		}
	}
	
	/// Forces to reconnect to server ASAP
	public void forceReconnect() {
		AGLog.i(CommunicationService.TAG, "th:forceReconnect()");
		mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_RECONECT_NOW));
	}

	/// Disconnects
	public void disconnect() {
		AGLog.i(CommunicationService.TAG, "th:disconnect()");
		mInterthreadQueue.offer(new InterThreadMessage(InterThreadMessage.TYPE_DISCONNECT));
	}
	
	/// Sends packet (if connected)
	public void sendPacket(SendPacket packet) {
		InterThreadMessage itm = new InterThreadMessage(InterThreadMessage.TYPE_SEND_PACKET);
		itm.sendPacket = packet;
		
		mInterthreadQueue.offer(itm);
	}
	
	////////////////////////////////////////////////////
	// implementation
	
	/// Handles received packet
	private void handlePacket(final ReceivedPacket packet) {
		// simply send yo ui thread
		AGLog.i(CommunicationService.TAG, "th: sending packet to ui thread: " + packet);
		
		mService.sendToUiThread(new Runnable(){
			public void run() { mService.onPacket(packet); }
		});
	}
	
	
	/// Closes channel, joins thread
	static private void closeChannelAndJoin(SocketChannel channel, Thread t) {
		try {
			if (channel != null) {
				AGLog.d(CommunicationService.TAG, "th: closing channel...");
				channel.close();
				AGLog.d(CommunicationService.TAG, "th: channel closed");
			}
			if (t != null) {
				AGLog.d(CommunicationService.TAG, "th: joining thread...");
				t.interrupt(); // TODO this is hack, i HATE it!
				t.join(1000); // 1 second timeout
				if (t.isAlive()) {
					AGLog.e(CommunicationService.TAG, "th: failed to join thread");
				}
				AGLog.d(CommunicationService.TAG, "th: thread joined");
			}
		} catch (Exception e) {
			AGLog.e(CommunicationService.TAG, "th: error closing channel: " + e);
			; // do nothing
		}
	}
	
	// Tries connecting. Sets status accordingly
	// returns channel if connected, null if not
	private SocketChannel tryConnecting() {
		sendConnectionState(ConnectionState.Connecting());
		
		// read uin and pwd
		Config config = App.getInstance().getConfig();
		if (config.hasLoginDetails()) {
			
			long uin = config.getUin();
			String password = config.getPassword();
			
			// try connecting
			try {
				AGLog.i(CommunicationService.TAG, "th:tryConnect: connecting with uin=" + uin + ", pwd=" + password);
				return mConnector.connect(uin, password, config.getStatus());
			} catch (Connector.LoginException e) {
				mReconnect = false;
				sendConnectionState(ConnectionState.Disconnected(ConnectionState.REASON_LOGIN_FAILED));
				AGLog.e(CommunicationService.TAG, "th:tryConnect: login failed: " + e.getMessage());
				return null;
			} catch (Connector.NetworkException e) {
				mReconnect = true; // will try to reconnect after some time
				AGLog.e(CommunicationService.TAG, "th:tryConnect: network error: " + e.getMessage());
				return null;
			} catch (Connector.ProtocolException e) {
				mReconnect = true; // will try to reconnect after some time
				AGLog.e(CommunicationService.TAG, "th:tryConnect: protocol error: " + e.getMessage());
				return null;
			} catch (Connector.RejectException e) {
				mReconnect = false; // will try to reconnect after some time
				AGLog.e(CommunicationService.TAG, "th:tryConnect: connection rejected: " + e.getMessage());
				return null;
			} catch (Connector.ConnectorException e) {
				; // java sux
			}
		}
		
		
		return null;
	}
	
	// sends connection state to UI thread to service
	private void sendConnectionState(final ConnectionState state) {
		mService.sendToUiThread(new Runnable() {
			public void run() { mService.onConnectionStateChange(state); }
		});
		
	}

}
